package com.mon4cc.simulation;

import com.mon4cc.cmon.adaption.BPMNAdaptor;
import com.mon4cc.cmon.model.event.EventAttrContext;
import com.mon4cc.cmon.model.event.IEventAttrContext;
import com.mon4cc.simulation.utils.BpmnUtils;
import lombok.val;
import org.jbpt.pm.DataNode;
import org.jbpt.pm.FlowNode;
import org.jbpt.pm.IDataNode;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

import java.net.IDN;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author Ping
 * @date 2022/12/19 11:32
 **/

public class SimulationContext extends ConcurrentHashMap<String, Object> implements ExecuteContext {
    /**
     * use Flyweight pattern
     */
    private Map<FlowNode, FlowNodeSimulatedInstance> simulatedInstanceFlyWeights;
    private LinkedList<FlowNodeSimulatedInstance> tokenEnabledNodes ;
    private SimulationConfig sconfig ;
    private long currentNumOfEvents ;
    private long currentNumForCurrentCase ;
    private Map<Long, Long> eventNumForEachCase ;
    private Map<Long, Long> errorNumForEachCase ;

    public SimulationContext(SimulationConfig sconfig) {
        this.tokenEnabledNodes = new LinkedList<FlowNodeSimulatedInstance>() ;
        this.simulatedInstanceFlyWeights = new ConcurrentHashMap<>() ;
        this.sconfig = sconfig ;
        this.currentNumOfEvents = 0 ;
        this.eventNumForEachCase = new ConcurrentHashMap<>() ;
        this.currentNumForCurrentCase = 0 ;
        this.errorNumForEachCase = new ConcurrentHashMap<>() ;
    }

    public SimulationContext(Map<String, Object> initialParams, SimulationConfig sconfig ) {
        super(initialParams) ;
        this.tokenEnabledNodes = new LinkedList<FlowNodeSimulatedInstance>() ;
        this.simulatedInstanceFlyWeights = new ConcurrentHashMap<>() ;
        this.sconfig = sconfig ;
        this.currentNumOfEvents = 0 ;
        this.eventNumForEachCase = new ConcurrentHashMap<>() ;
        this.currentNumForCurrentCase = 0 ;
        this.errorNumForEachCase = new ConcurrentHashMap<>() ;
    }


    @Override
    public FlowNodeSimulatedInstance getSimulationInstance(FlowNode flowNode) {
        return simulatedInstanceFlyWeights.get(flowNode) ;
    }

    /**
     * get next enabled instance randomly by shuffle method.
     * @return
     */
    @Override
    public FlowNodeSimulatedInstance getNextEnabledInstance() {
        Collections.shuffle(this.tokenEnabledNodes);
        return this.tokenEnabledNodes.poll();
    }

    /**
     * update data context by current instance
     * @param currentInstance
     */
    @Override
    public void updateDataContext(FlowNodeSimulatedInstance currentInstance) {
        FlowNode flowNode = currentInstance.getFlowNode() ;
        Collection<IDataNode> dataNodes = flowNode.getWriteDocuments() ;
        if (! ObjectUtils.isEmpty(dataNodes)) {
            dataNodes.forEach(dNode -> {
                this.put(dNode.getName(),
                              sconfig.getDataRangerMap().get(dNode.getName()).nextValue()) ;
            }) ;
        }

    }

    @Override
    public void updateDataContext(Map<String, Object> dataValues) {
        this.putAll(dataValues) ;
    }

    @Override
    public void updateEnabledSimulatedInstance(FlowNodeSimulatedInstance currentInstance, Set<FlowNode> nextCandidateFlowNode) {
        Assert.state(! ObjectUtils.isEmpty(nextCandidateFlowNode), "next candidate flow nodes can not be empty!");
        nextCandidateFlowNode.forEach(next -> updateEnabledSimulatedInstance(currentInstance, next));
    }

    @Override
    public void updateEnabledSimulatedInstance(FlowNodeSimulatedInstance currentInstance, FlowNode nextCandidate) {
        FlowNodeSimulatedInstance instance = null ;
        if (! this.simulatedInstanceFlyWeights.containsKey(nextCandidate)) {
            instance = BpmnUtils.toInstance(nextCandidate, currentInstance.bpmnSimulation) ;
            this.simulatedInstanceFlyWeights.put(nextCandidate, instance) ;
        } else {
            instance = this.simulatedInstanceFlyWeights.get(nextCandidate) ;
        }
        // passing token from current instance to candidate instances
        instance.getTokenFrom(currentInstance);
        if (instance.isEnabledNow()) {
            this.tokenEnabledNodes.add(instance);
        }
    }

    @Override
    public boolean endEventArrived() {
        return this.simulatedInstanceFlyWeights.keySet().stream().anyMatch(BPMNAdaptor::isEndEvent);
    }

    @Override
    public boolean needToSwapped(FlowNodeSimulatedInstance currentInstance) {
        return  this.sconfig.needToSwapped(currentInstance.getName());
    }

    @Override
    public String getSwappedInstanceName(FlowNodeSimulatedInstance currentInstance) {
        Assert.state(this.sconfig.needToSwapped(currentInstance.getName()), "the current node should need to be swapped.") ;
        return this.sconfig.swappedTo(currentInstance.getName()) ;
    }

    @Override
    public boolean needToAddArtificalInstance(FlowNodeSimulatedInstance currentInstance) {
        return this.sconfig.isNeedInsertArtificialEvent(currentInstance.getName());
    }

    @Override
    public boolean generatedEnoughEvents() {
        return this.currentNumOfEvents >= this.sconfig.getTotalNumOfEvents();
    }

    @Override
    public Object get(String attr) {
        return super.get(attr);
    }

    @Override
    public long getCaseId() {
        return (Long) get(ExecuteContext.NameOfCaseId);
    }

    @Override
    public void setCaseId(long caseId) {
        this.put(ExecuteContext.NameOfCaseId, caseId) ;
        // reset current number of event
        this.currentNumForCurrentCase = 0 ;
        this.eventNumForEachCase.put(caseId,  this.currentNumForCurrentCase) ;
    }

    @Override
    public Map<String, Object> currentDataContext() {
        return this ;
    }

    @Override
    public void release() {
        this.tokenEnabledNodes.clear() ;
        this.simulatedInstanceFlyWeights.clear() ;
    }

    @Override
    public void reset(Map<String,Object> initialParams) {
        this.eventNumForEachCase.put(this.getCaseId(), this.currentNumForCurrentCase) ;
        super.clear() ;
        super.putAll(initialParams) ;
        this.tokenEnabledNodes.clear() ;
        this.simulatedInstanceFlyWeights.clear() ;
        this.currentNumForCurrentCase = 0 ;
    }

    @Override
    public IEventAttrContext attrContext() {
        return new EventAttrContext(this) ;
    }

    @Override
    public void increaseTotalNum() {
        this.currentNumOfEvents ++ ;
        this.currentNumForCurrentCase ++ ;
    }

    @Override
    public Map<Long, Long> getTraceLengthForEachCase() {
        return this.eventNumForEachCase;
    }

    @Override
    public Map<Long, Long> getErrorMap() {
        return this.errorNumForEachCase;
    }

    @Override
    public void increaseErrorNum() {
        Long caseId = this.getCaseId() ;
        Assert.notNull(caseId, "Case id should not null.");
        if (this.errorNumForEachCase.containsKey(caseId)) {
            this.errorNumForEachCase.put(caseId, this.errorNumForEachCase.get(caseId) + 1) ;
        } else {
            this.errorNumForEachCase.put(caseId, 1L) ;
        }
    }
}
